package com.tiglle.lettuce.controller;

import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Random;

/**
 * 秒杀案例：一个商品有n个库存，n个用户开始秒杀，一个用户只能秒杀一个
 * 一个用户只能秒杀一次
 *              ：用户秒杀后，放入set中：sadd user:productId userId
 *  可使用ab测试并发，ab工具安装方法:
 *              1.yum -y install httpd-tools
 *              编写参数文件
 *              2.vim postfile
 *              内容格式：参数名=参数值&....
 *              productId=1
 *              执行命令:
 *              3.ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://192.168.6.1/seckill
 *                      -n:总请求数
 *                      -c:一次请求的并发数
 *                      相当于：运行10次，每次并发200
 *              查看参数说明,输入ab就行：
 *              4.ab
 *
 */
@RestController
public class SecKillController {

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 普通秒杀（没有事务，没有锁）
     * 问题：
     * 1.没有事务，库存-1后，如果标记用户时异常，库存不会回滚
     * 2.库存超卖，有负数的情况：并发情况下，an个线程将要扣减库存，还没扣减的时候，bn个线程此时来执行库存判断，发现有库存，往下执行，此时an菜扣减库存，bn也会进行库存扣减
     * 3.
     * @param productId
     * @return
     */
    @GetMapping("seckill")
    public String seckill(String productId,String userId){
        if(!StringUtils.hasLength(userId)){
            //随机一个用户id,参数的用来测试
            userId = new Random().nextInt(5000)+"";
        }
        //库存key，和存userId的set的key
        String kuCunKey = "kucun:"+productId;
        String userIdSetKey = "user:"+productId;
        //参数校验，userId和productId的非空判断
        if(!StringUtils.hasLength(userId)||!StringUtils.hasLength(productId)){
            System.out.println("参数校检失败");
            return "参数校检失败";
        }
        //获取库存
        Object kucun = redisTemplate.opsForValue().get(kuCunKey);
        if(Objects.isNull(kucun)){
            System.out.println("秒杀未开始");
            return "秒杀未开始";
        }
        //库存数小于1，秒杀结束
        int kucuned = Integer.parseInt(kucun.toString());
        if(kucuned<1){
            System.out.println("秒杀结束");
            return "秒杀结束";
        }
        //一个用户只能秒杀一次，判断set中有无指定的值
        Boolean userSeckilled = redisTemplate.opsForSet().isMember(userIdSetKey, userId);
        if(userSeckilled){
            System.out.println("此用户你已经秒杀过此商品了");
            return "此用户你已经秒杀过此商品了";
        }
        //开始秒杀，扣减库存，秒杀过的用户放入set中
        redisTemplate.opsForValue().decrement(kuCunKey);
        redisTemplate.opsForSet().add(userIdSetKey,userId);
        System.out.println("用户"+userId+"秒杀成功!!");
        return "success";
    }

    /**
     * 乐观锁+事务 秒杀 解决超卖问题
     * 只使用事务，不使用乐观锁，超卖会更加严重
     * 问题：库存遗留，商品库存没秒杀完
     * 原因：高并发下，an个线程watch库存后，往下执行，还没进行库存操作，bn个线程又对库存进行了watch，然后an个线程成功扣减库存，此时bn个线程在执行扣减时，发现库存值被修改过，事务会被取消执行
     * @param productId
     * @param userId
     * @return
     */
    @GetMapping("seckillOptlock")
    public String seckillOptlock(String productId,String userId){
        if(!StringUtils.hasLength(userId)){
            //随机一个用户id,参数的用来测试
            userId = new Random().nextInt(5000)+"";
        }
        //库存key，和存userId的set的key
        String kuCunKey = "kucun:"+productId;
        String userIdSetKey = "user:"+productId;
        //参数校验，userId和productId的非空判断
        if(!StringUtils.hasLength(userId)||!StringUtils.hasLength(productId)){
            System.out.println("参数校检失败");
            return "参数校检失败";
        }

        //开始事务方式执行
        String finalUserId = userId;
        Object returnStr = redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                //监听库存
                operations.watch(kuCunKey);
                //获取库存
                Object kucun = redisTemplate.opsForValue().get(kuCunKey);
                if (Objects.isNull(kucun)) {
                    System.out.println("秒杀未开始");
                    return "秒杀未开始";
                }
                //库存数小于1，秒杀结束
                int kucuned = Integer.parseInt(kucun.toString());
                if(kucuned<1){
                    System.out.println("秒杀结束");
                    return "秒杀结束";
                }
                //一个用户只能秒杀一次，判断set中有无指定的值
                Boolean userSeckilled = redisTemplate.opsForSet().isMember(userIdSetKey, finalUserId);
                if(userSeckilled){
                    System.out.println("此用户你已经秒杀过此商品了");
                    return "此用户你已经秒杀过此商品了";
                }
                //开启事务 operations操作和redisTemplate操作都会加入事务列队
                operations.multi();
                //开始秒杀，扣减库存，秒杀过的用户放入set中
                redisTemplate.opsForValue().decrement(kuCunKey);
                redisTemplate.opsForSet().add(userIdSetKey, finalUserId);
                //执行事务
                List exec = operations.exec();
                if(CollectionUtils.isEmpty(exec)){
                    return "秒杀失败";
                }
                System.out.println("用户"+ finalUserId +"秒杀成功!!");
                return "秒杀成功";
            }
        });
        return returnStr.toString();
    }

    /**
     * 调用lua脚本，解决库存遗留：redis2.6以上才能运行
     *lua：保证原子性，lua脚本在执行的时候，不会有其他脚本和命令同时执行
     * @param productId
     * @param userId
     * @return
     */
    @GetMapping("seckillOptlockLua")
    public String seckillOptlockLua(String productId,String userId){
        if(!StringUtils.hasLength(userId)){
            //随机一个用户id,参数的用来测试
            userId = new Random().nextInt(5000)+"";
        }
        //参数校验，userId和productId的非空判断
        if(!StringUtils.hasLength(userId)||!StringUtils.hasLength(productId)){
            System.out.println("参数校检失败");
            return "参数校检失败";
        }
        String secKillScript =//声明参数
                                        "local userId=KEYS[1];\r\n" +
                                        "local produceId=KEYS[2];\r\n" +
                                        //声明key
                                        "local kuCunKey='kucun:'..produceId;\r\n" +
                                        "local userIdSetKey='user:'..produceId;\r\n" +
                                         //判断用户是否已经秒杀过（set中有用户id）
                                        "local userExists=redis.call(\"sismember\",userIdSetKey,userId);\r\n" +
                                         //如果用户存在，返回2
                                        "if tonumber(userExists)==1 then \r\n" +
                                        "   return 2;\r\n" +
                                        "end\r\n" +
                                         // 获取库存数量
                                        "local num= redis.call(\"get\" ,kuCunKey);\r\n" +
                                        //如果库存数量小于等于0，返回0
                                        "if tonumber(num)<=0 then \r\n" +
                                        "   return 0;\r\n" +
                                        "else \r\n" +
                                        //如果库存数量大于0，扣减库存-1 ，并将userId放入set中，然后返回1
                                        "   redis.call(\"decr\",kuCunKey);\r\n" +
                                        "   redis.call(\"sadd\",userIdSetKey,userId);\r\n" +
                                        "end\r\n" +
                                        "return 1" ;
        DefaultRedisScript<Long> luaScript = new DefaultRedisScript<>(secKillScript,Long.class);
        //最有一个参数时脚本中ARGV[1]所需要的参数，但是这里没有用带，传null会空指针，用new Object()也会报错..
        Object execute = redisTemplate.execute(luaScript, Arrays.asList(userId, productId), "");
        long result = Long.parseLong(execute.toString());
        if(result==2){
            System.out.println("用户已经秒杀过此商品");
            return "用户已经秒杀过此商品";
        }
        if(result==0){
            System.out.println("秒杀未开始");
            return "秒杀未开始";
        }
        if(result==1){
            System.out.println("秒杀成功");
            return "秒杀成功";
        }
        System.out.println(execute);
        return "success";
    }
}
